
class PDF:
    def __init__(self, pdfPath):
        self.Path = abspath(pdfPath)
        if not exists(self.Path):
            raise IOError('file {} not found'.format(self.Path))

    @property
    def filename(self):
        _, name = split(self.Path)
        return name

    @property
    def title(self):
        if 'Title' in self.__dict__:
            return self.Title
        name, _ = splitext(self.filename)
        return name

    @property
    def metadata(self):
        if 'MetaData' not in self.__dict__:
            self.MetaData = self.read_metadata()
        return self.MetaData

    @metadata.setter
    def metadata(self, metadata):
        lines = []
        for key, value in metadata.items():
            if isinstance(value, list):
                for item in value:
                    line = '{}Begin\n'.format(key)
                    lines.append(line)
                    for k, v in item.items():
                        line = '{}{}: {}\n'.format(key, k, v)
                        lines.append(line)
            else:
                line = '{}: {}\n'.format(key, value)
                lines.append(line)
        self.update_metadata(lines)

    @property
    def num_pages(self):
        if 'NumberOfPages' in self.metadata:
            return int(self.metadata['NumberOfPages'])
        return 0

    @property
    def bookmarks(self):
        if 'Bookmark' in self.metadata:
            return self.metadata['Bookmark']
        else:
            return []

    @bookmarks.setter
    def write_bookmarks(self, bookmarks):
        self.metadata['Bookmark'] = bookmarks

    def __repr__(self):
        return 'PDF({})'.format(self.filename)

    def parse(self, lines):
        prefix, item, metadata = None, None, {}
        for line in lines:
            prefix, item, metadata = self.parse_line(line, prefix, item, metadata)
        metadata = self.add_item(prefix, item, metadata)
        return metadata

    def add_item(self, prefix, item, metadata={}):
        if prefix is None:
            return metadata
        if prefix not in metadata:
            metadata[prefix] = []
        if item is not None:
            metadata[prefix].append(item)
        return metadata

    def parse_line(self, line, prefix, item, metadata={}):
        res = re.match('(.*)Begin', line)
        if res:
            metadata = self.add_item(prefix, item, metadata)
            prefix = res.groups()[0]
            item = {}
        else:
            if prefix is None:
                pattern = '(.*):\s*(.*)\n'
                res = re.match(pattern, line)
                if res:
                    name = res.groups()[0]
                    value = res.groups()[1]
                    if name == 'NumberOfPages':
                        value = int(value)
                    metadata[name] = value
                else:
                    raise IOError('line = {}, pattern = {}'.format(line, pattern))
            else:
                if line[:len(prefix)] == prefix:
                    pattern = prefix + '([^:]*):\s*(.*)\n'
                    res = re.match(pattern, line)
                    if res:
                        name = res.groups()[0]
                        value = res.groups()[1]
                        if name == 'PageNumber' or name == 'Level':
                            value = int(value)
                        try:
                            item[name] = value
                        except:
                            raise IOError('line = {}, pattern = {}'.format(line, pattern))
                    else:
                        raise IOError('line = {}, pattern = {}'.format(line, pattern))
                else:
                    metadata = self.add_item(prefix, item, metadata)
                    item = None
                    prefix = None
        return prefix, item, metadata

    def read_metadata(self, out_file=None, isUTF8=True):
        with WorkingDir(self.Path):
            if out_file is None:
                out_file_relative = mktemp('.txt')
            else:
                out_file_relative = relpath(out_file)
            in_file_relative = relpath(self.Path)
            try:
                assert(exists(in_file_relative))
                pattern = '{} {} dump_data{} output {}'
                utf8 = '_utf8' if isUTF8 else ''
                cmd = pattern.format(pypdftk.PDFTK_PATH, in_file_relative, utf8, out_file_relative)
                pypdftk.check_output(cmd)
                assert(exists(out_file_relative))
            except Exception as e:
                print(e)
            with open(out_file_relative, encoding='utf8') as fd:
                lines = fd.readlines()
                metadata = self.parse(lines)
            if out_file is None:
                remove(out_file_relative)
            return metadata

    def update_metadata(self, lines, output_txt=None, isUTF8=True):
        with WorkingDir(self.Path):
            if output_txt is None:
                output_txt_relative = mktemp('.txt')
            else:
                output_txt_relative = relpath(output_txt)
            with open(output_txt_relative, 'w') as fd:
                fd.writelines(lines)
            input_pdf = mktemp('.pdf')
            copyfile(self.Path, input_pdf)
            output_pdf = relpath(self.Path)
            cmd = '{} {} update_info{} {} output {}'.format(
                pypdftk.PDFTK_PATH, input_pdf,
                '_utf8' if isUTF8 else '',
                output_txt_relative, output_pdf)
            pypdftk.run_command(cmd)
            remove(input_pdf)
            remove(output_txt_relative)


def cat_pdfs(pdfList, out_file):
    try:
        pdfList = [relpath(f.Path) if isinstance(f, PDF) else relpath(f) for f in pdfList]
        temp = mktemp('.pdf')
        print('running pdftk {} cat output {}'.format(pdfList, temp))
        pypdftk.concat(pdfList, out_file=temp)
        move(temp, out_file)
        assert(exists(out_file))
        return PDF(out_file)
    except:
        raise

def merge_bookmarks(titles, bookmarks, num_pages):
    start_page = 1
    out_bookmarks = []
    for title, Bookmark, pages in zip(titles, bookmarks, num_pages):
        out_bookmarks.append({
            'Title': title,
            'Level': 1,
            'PageNumber': start_page
        })
        for current in Bookmark:
            current['Level'] = current['Level'] + 1
            current['PageNumber'] = current['PageNumber'] + start_page - 1
            out_bookmarks.append(current)
        start_page = start_page + pages
    return out_bookmarks

def merge_metadata(pdfs):
    pdfs = [f if isinstance(f, PDF) else PDF(f) for f in pdfs]
    titles = [f.title for f in pdfs]
    num_pages = [f.num_pages for f in pdfs]
    bookmarks = [f.bookmarks for f in pdfs]
    metadata = {}
    metadata['NumberOfPages'] = sum(num_pages)
    merged_bookmarks = merge_bookmarks(titles, bookmarks, num_pages)
    metadata['Bookmark'] = merged_bookmarks
    return metadata

def merge_pdfs(pdfs, out_file):
    if len(pdfs) > 0:
        assert(out_file is not None)
        out_file = cat_pdfs(pdfs, out_file)
        assert(isinstance(out_file, PDF))
        out_file.metadata = merge_metadata(pdfs)
        return out_file

